Whether you write programs for users in your own company, for the public at large or just for enjoyment, you probably got into programming because you like the challenge. And, if you're like me, each time you write a program you try to make it better than the last program or you try to make it better than the other programs you see in the market.
In this article, I'm going to show you several pieces of code -- some of them solve a particular problem and others add a little pizzazz to your programs and make them stand out from the crowd.
Tip 1 -- Avoid using Shell to copy files
It's easy to use VB's Shell command to call DOS when you want to copy a file:
X = Shell("command.com /C COPY \AUTOEXEC.BAT \AUTOEXEC.BAK")
The Shell command doesn't give your program direct access to any errors that it encounters.
As an alternative, the function shown below can return information to your program should an error occur as your program tries to copy a file.
BufferLen& = 28 * 1024& 'Set to a large string length
' Open files
InFileNbr% = FreeFile 'Get next available file number
Open InFile$ For Binary Access Read Lock Read Write As InFileNbr%
OutFileNbr% = FreeFile 'Get next available file number
Open OutFile$ For Binary Access Write Lock Read Write As OutFileNbr%
' Establish buffer length
InFileLen& = LOF(InFileNbr%)
If InFileLen& < BufferLen& Then
BufferLen& = InFileLen&
End If
Buffer$ = Space$(BufferLen&) 'Initialize buffer
Do 'Copy as much of file as possible in large blocks
Get #InFileNbr%, , Buffer$
Put #OutFileNbr%, , Buffer$
Loop While (Not EOF(InFileNbr%)) And (InFileLen& >= (Loc(InFileNbr%) + BufferLen&))
If Not EOF(InFileNbr%) Then ' Copy rest of file if any
Buffer$ = Space$(InFileLen& - Loc(InFileNbr%))
Get #InFileNbr%, , Buffer$
Put #OutFileNbr%, , Buffer$
End If
CopyFile% = -1 ' Function ended successfully
ErrorNbr% = 0 ' No error number to return
LeaveFunction:
Close #InFileNbr%, OutFileNbr%
Exit Function
ErrorCopyingFile:
CopyFile% = 0 'Return value that indicates function error
ErrorNbr% = Err 'Return specific error code
Resume LeaveFunction
End Function
As you can see, the function requires three parameters: InFile$ is the name of the source file, OutFile$ is the name of the destination file, and ErrorNbr% is the Basic error number the CopyFile% routine returns to your program if it encounters an error.
Notice that we opened the input and output files as binary files. Doing so lets us copy any type of file easily--both text and executable files. Additionally, we restricted access to both the source and destination files during the copy process by setting the lock status in the Open instruction to "Read Write." This prevents any other process from reading from the file or writing to the file while we try to copy it.
To keep the routine as general as possible, we use the FreeFile function to assign file numbers for our Open statement. This means that the file numbers used by CopyFile won't conflict with any file numbers in use by your program. Lastly, you'll see that we initially try to set the buffer to a large size (28Kbytes) in order to copy the file with as few passes as possible. However, if the source file is smaller than this initial size, we reset the buffer size to match the file size.
To copy the file, we read a portion of the file into Buffer$ then write it to the destination file. We repeat this process until we reach the end of the file or have a portion of the file smaller than Buffer$ left to copy to the destination file. If a small portion of the file remains at the end of this process, we reset the size of Buffer$ to match the size of the rest of the file, then copy that portion to the destination file. Once the source file has been copied, we set the return values and exit the function.
Your program should check the function's value after it returns to make sure it didn't encounter an error. For example, to test the CopyFile% function, we created a simple project that contained two text box controls--one for the source filename and one for the destination filename. We placed the code shown below into the Form_Click event procedure.
IFile$ = Text1.Text 'Source filename
OFile$ = Text2.Text 'Destination filename
Result% = CopyFile%(IFile$, OFile$, ErrorCode%)
If Result% Then 'Check the return value
Text3.Text = "File copied successfully."
Else
Text3.Text = "Error during copy. " + Error$(ErrorCode%) + "."
End If
When you click on the form, the above code calls the CopyFile% function. If CopyFile% returns True (-1), we display a message indicating success in a third text box control. If CopyFile% returns False (0), we display a message with the text of the error.
Tip 2 -- Use Windows API to get a file's date and time stamp
You can use either the VB Dir$ command or the file list box to retrieve the names of files, but neither one will give you the date or time stamp of the file. Fortunately, the Windows API routine OpenFile returns this information, provided you know how to decode some undocumented bytes. The declaration statement for the function and a couple of constant values we'll use are shown below. They're easiest to use when placed in your "global" module.
Declare Function OpenFile Lib "Kernel" (ByVal lpFileName As String, lpReOpenBuff As OFSTRUCT, ByVal wStyle As Integer) As Integer
Global Const OF_EXIST = &H4000
Global Const HFILE_ERROR = -1
You pass OpenFile the name of the file in which you're interested in the first parameter--lpFileName. The second parameter contains a record of the type OFSTRUCT.
Type OFSTRUCT
cBytes As String * 1 'Length of structure
fFixedDisk As String * 1 '0 if file on removable disk
nErrCode As Integer 'MS-DOS error code if routine fails
FileDate As String * 2 'Modified reserved field
FileTime As String * 2 'Modified reserved field
szPathName As String * 128 'Complete pathname of file
End Type
Before you call the OpenFile function to retrieve this information you must initialize the cBytes field to the length of the record. When you pass this structure to the API OpenFile function, Windows fills in the fields with the values for the file in question. In the original OFSTRUCT record (as defined by Microsoft) the FileDate and FileTime fields are listed as a single reserved field four bytes long. We redefined the field into two fields named FileDate and FileTime to make it easier to decode the date and time values.
In the last parameter position--wStyle, you pass OpenFile a value that indicates what action you want the routine to perform. While the OpenFile function lets you access files in a variety of ways, to get the file's date and time stamps you pass it the constant OF_EXIST -- that tells it to simply verify that the file exists and fill in the fields of the OFSTRUCT with the information about the file.
If the function fails, it returns the constant HFILE_ERROR. Additionally, the nErrCode field of the OFSTRUCT record will contain the MS-DOS error code number that corresponds to the error.
To call the OpenFile function and decode the date and time values, we placed the instructions
DefInt A-Z
Dim OFS As OFSTRUCT
into the declarations section of a form, then attached the code below to the Click event procedure of a command button.
Sub Command1_Click ()
FileName$ = Text1.Text 'Get filename from text box 1
This code in the Click event procedure expects you to type the filename into the Text1 text box control. Once the code is activated, it calls OpenFile, decodes the date and time values, then displays the results in the Text2 text box control.
To get as much information into two bytes as it can, DOS uses the minimum number of bits required to store the day, month, year, hour, minute, and second values in each byte. For example, the file day number is stored in bits 0-4 of the FileDate field. To get the actual date and time values, we had to isolate the ranges of bits in the FileDate and FileTime fields corresponding to each value. These ranges are
FileTime
Bits Purpose Range
0-4 Seconds (in two-second intervals) 0-29
5-10 Minutes 0-59
11-15 Hours 0-23
FileDate
Bits Purpose Range
0-4 Day 1-31
5-8 Month 1-12
9-15 Year Relative to 1980
We formatted the date and time values into the strings DateStr$ and TimeStr$. You may find that it's more convenient to create separate functions to get the date and time stamps.
Tip 3 -- Take advantage of DOS paths
One way to make your programs easier to run is to avoid forcing users to find files. If a file is located in a directory whose name is listed on the Path environment variable, your program should be able to find the file for the user automatically. The FindFileInPath% function, shown below does just that.
Function FindFileInPath% (TypePath$, FileName$)
'Parameters
'Input:
' TypePath$ -- The NAME of Either the PATH or APPEND environment
' variable or any other environment variable name
' FileName$ -- The filename to search for. Assumes a
' valid filename
'
'Output:
' FileName$ -- Full pathname for file
'
'Return value:
' True (-1) -- If successful
' False (0) -- File not found in path
PathValue$ = Environ$(TypePath$)
If Len(PathValue$) = 0 Then
Exit Function ' Environment variable name not passed
End If
StartPos% = 1
'Append semi-colon to path if needed so there are fewer
'conditions to check in code
If Right$(PathValue$, 1) <> ";" Then PathValue$ = PathValue$ + ";"
DelimPos% = InStr(StartPos%, PathValue$, ";")
While DelimPos% 'While there are still directories to check
StartPos% = DelimPos% + 1 'Get ready to search next directory
DelimPos% = InStr(StartPos%, PathValue$, ";")
Wend
End Function
To use the function, first place it into a module or form. When you want to find a file, pass the name of the environment variable to search in the first parameter and the name of the file to find in the second parameter. The return value for FindFileInPath% is True (-1) if it found the file -- in this case it returns the complete pathname for the file in the FileName$ parameter. If the function doesn't find the file, it returns a value of False (0).
Notice that we wrote the function so that it searches the name of any environment variable you pass it. This allows you to search the directories listed in the Path and the Append environment variables easily. Additionally, with a few modifications--beginning with passing the appropriate path to the function instead of looking the path value up in the function itself--you can make this routine search custom "path" strings for each of your applications.
Tip 4 -- Add MKI$ and CVI% functions
If you've been programming in Basic for a while, you've probably used the MKI$ and CVI% functions. You may remember that the MKI$ function converts an integer value from a numeric representation to a string representation and the CVI% converts the string form of an integer to a numerical value. When Microsoft created the list of keywords for the Visual Basic language, they omitted these two functions. This causes problems if you have data files that depend on these functions in order to access your data.
Fortunately, you can use the two user-defined functions shown below in place of the missing standard functions.
FUNCTION CVI% (I$)
Byte1$ = LEFT$(I$, 1)
Byte2$ = MID$(I$, 2, 1)
TempI& = ASC(Byte2$) * 256& + ASC(Byte1$)
IF TempI& > 32767 THEN
TempI& = TempI& - 65536
END IF
CVI% = TempI&
END FUNCTION
FUNCTION MKI$ (I%)
TempI& = I%
IF TempI& < 0 THEN
TempI& = TempI& + 65536
END IF
Byte1% = TempI& \ 256
Byte2% = TempI& MOD 256
C1$ = CHR$(Byte1%)
C2$ = CHR$(Byte2%)
MKI$ = C2$ + C1$
END FUNCTION
While we're not going to go into detail about the internal storage format of these representations, if you examine the code, you can see that we took special care to make sure that the functions work for both positive and negative values in the integer range. To use these functions, just place their code into a form or module. They can be called by any routine within their scope.
Tip 5 -- Use Enter instead of Tab to move from control to control
One of the things many users detest--especially in an environment like Windows--is learning different techniques for common operations. For example, if a user is accustomed to pressing the <Enter> key in one application to move from field to field and then is forced to use the <Tab> key instead, they may never become comfortable switching between the two programs.
An alternative is to modify your VB programs so that they work the way your users do. In this case, you can change the actions of your controls so that focus shifts to the next control when you press the <Enter> key. First place the definition of the KEY_RETURN constant into the declarations section of a form (or as a global constant in your global file).
Const KEY_RETURN = &HD
Next, place the code shown below into the KeyPress event procedure of any control that you want to transfer focus when the <Enter> key is pressed.
Sub Text1_KeyPress (KeyAscii As Integer)
If KeyAscii = KEY_RETURN Then
KeyAscii = 0
SendKeys "{Tab}"
End If
End Sub
Essentially, we check each character (passed to us in the KeyAscii parameter) to see if the <Enter> key was pressed. If so, we set the KeyAscii value to zero (so that the keypress is ignored by other VB routines). Finally, we execute a SendKeys statement to simulate pressing the Tab key, which causes the focus to move to the next control that can accept focus.
Tip 6 -- Preset the cursor position for the user
Computer users always appreciate anything you can do to make it easier for them to work with your program. For instance, if you know that the cursor needs to be at a specific location on a form why not move it there for the user's convenience? The Windows API provides two routines you'll find helpful if you want to add this functionality to your programs. The declarations for these routines are:
Declare Sub ClientToScreen Lib "User" (ByVal hWnd As Integer, lpPoint As POINTAPI)
Declare Sub SetCursorPos Lib "User" (ByVal X As Integer, ByVal y As Integer)
The ClientToScreen routine converts a position on your form to a position on the screen, which is the format required by the SetCursorPos routine. The SetCursorPos routine positions the hotspot of the cursor to the location you specify. If you examine the declaration for the ClientToScreen function, you'll see that the second parameter is a record of the type POINTAPI. Windows uses this record type to store the coordinates for many objects. The definition for the record is
Type POINTAPI
X As Integer
Y As Integer
End Type
(Place this definition into your global module.)
Once you've added the declarations to your program, you're ready to change the position of the cursor. To get an idea of how these functions work, suppose you want to move the cursor to the middle of the Command1 button when you leave the Text1 text box control. To do this, just add the code shown below to the LostFocus event procedure for Text1.
Sub Text1_LostFocus ()
Dim P As POINTAPI
SaveMode% = Form1.ScaleMode
Form1.ScaleMode = 3
P.X = Command1.Left + (Command1.Width) / 2
P.Y = Command1.Top + (Command1.Height) / 2
ClientToScreen Form1.hWnd, P
SetCursorPos P.X, P.Y
Form1.ScaleMode = SaveMode%
End Sub
When Text1 loses focus, VB activates the code in this event procedure. The first instruction dimensions a record named P (of type POINTAPI), that we use to store the cursor position. Because we change the form's scalemode to pixels, we then save the current ScaleMode value and then restore it when we're done. The next instruction sets the scalemode of the form to Pixels to match the scalemode used by the screen. The next two instructions calculate the new position for the cursor, based on the location of the Command1 control. As you can see, we decided to center the cursor on the button. The next instruction converts our new cursor location from the relative coordinates of our form to the absolute screen coordinates required by the SetCursorPos routine in the next to last instruction.
Executing this code positions the cursor to the middle of the Command1 control, no matter what its previous location was. You'll probably find that your users appreciate the extra attention when you display the cursor in the best location as you start your application or present dialog boxes.
Tip 7 -- Keep cursors consistent from form to form
Many programmers use the MousePointer property to identify the type of functions users can perform based on the pointer's shape. If your application displays a number of forms, you may need to make sure that the pointer's shape stays consistant from form to form. Rather than set the MousePointer property for each of your forms with instructions like
Form1.MousePointer = 2 'Set to cross
you can set the type of pointer for all your forms with a single instruction:
Screen.MousePointer = 2 'Set pointer to cross for all forms
Just execute this instruction when you want to change the pointer shape and VB will display the chosen cursor for all your forms. Note that this technique only works for the forms associated with the application in which you execute this instruction. Other applications retain their own cursor shapes.
Mark Novisoff is president of MicroHelp Inc., a publisher of tools for BASIC programmers in Roswell, Georgia. Contact Mark on CompuServe at user ID 73047,3706.